%include "boot.inc"
SECTION LOADER vstart=LOADER_BASE_ADDR
LOADER_STACK_TOP 	equ	LOADER_BASE_ADDR

GDT_BASE:			dd	0x00000000
					dd	0x00000000
CODE_DESC:			dd	0x0000FFFF
					dd	DESC_CODE_HIGH4

DATA_STACK_DESC:	dd	0x0000FFFF
					dd	DESC_DATA_HIGH4
VIDEO_DESC:			dd	0x80000007
					dd	DESC_VIDEO_HIGH4

GDT_SIZE			equ	$ - GDT_BASE
GDT_LIMIT			equ	GDT_SIZE - 1
times		60 		dq 	0

SELECTOR_CODE		equ	(0x01 << 3) + TI_GDT + RPL0
SELECTOR_DATA		equ	(0x02 << 3) + TI_GDT + RPL0
SELECTOR_VIDEO		equ	(0x03 << 3) + TI_GDT + RPL0

; LOADER_BASE_ADDR也就是loader的起始地址是0x900
; 上面定义了64个8字节大小的数据 也就是512字节 即0x200
; 所以total_mem_bytes的地址应该是0x900+0x200=0xb00
total_mem_bytes		dd	0

gdt_ptr:			dw	GDT_LIMIT
					dd	GDT_BASE

ards_buf times 244  db 0
ards_nr 			dw 	0		      ;用于记录ards结构体数量


loader_start:
	mov byte [gs:160],'2'
	mov byte [gs:161],0xA4

	mov byte [gs:162],' '
	mov byte [gs:163],0xA4

	mov byte [gs:164], 'L'
	mov byte [gs:165],0xA4

	mov byte [gs:166], 'O'
	mov byte [gs:167],0xA4

	mov byte [gs:168],'D'
	mov byte [gs:169],0xA4

	mov byte [gs:170],'E'
	mov byte [gs:171],0xA4

	mov byte [gs:172],'R'
	mov byte [gs:173],0xA4

	; 通过bios的0x15中断中的0xe820功能号获取内存
	; 第一次调用时bx要清零
	mov ebx, 0
	; edx存放标记签名 固定是0x534d4150
	mov edx, 0x534d4150
	; di存放ards数组缓冲区地址
	mov di, ards_buf
.e820_get_ram_loop:
	; eax放入0xe820功能号
	mov eax, 0xe820
	; ecx中放入ards结构的大小，bios支持的是20字节，也许以后会拓展
	mov ecx, 20
	int 0x15
	jc get_ram_fuck
	; 每次回传20个字节，所以di每次加20
	add di, 20
	inc dword [ards_nr]
	cmp ebx, 0
	jnz .e820_get_ram_loop

	; 获取ards数量
	mov cx, [ards_nr]
	mov edx, 0
	; 将ards缓冲区地址放入di
	mov di, ards_buf
.e820_get_ram_max:
	; 获取ards描述符中的内存容量，因为是32位系统，所以只需要获取低32位
	mov eax, [di]	      ;base_add_low
    add eax, [di+8]	      ;length_low
	; 每次di偏移ards描述符大小，即20字节
	add di, 20
	; 比较edx内容是否大于eax
	cmp edx, eax
	jge	.next_e820_get_ram_max
	mov edx, eax
.next_e820_get_ram_max:
	loop .e820_get_ram_max
	
	jmp get_ram_ok

.e820_error_so_88:
	mov ah, 0x88
	int 0x15
	jc get_ram_fuck
	; 因为0x88功能号最大只能获取到63MB内存，所以只需要低4字节即可
	and eax, 0x0000ffff
	; 获取到的内容放入ax,以KB为单位，所以我们转换为字节
	mov cx, 1024
	mul cx
	shl edx, 16
	or edx, eax
	add edx, 0x100000

get_ram_ok:
	mov [total_mem_bytes], edx

	in al,	0x92
	or al, 0000_0010b
	out 0x92, al

	lgdt [gdt_ptr]

	mov eax, cr0
	or eax, 0x00000001
	mov cr0, eax

	jmp SELECTOR_CODE:loader_protection

get_ram_fuck:
	mov byte [gs:320], 'E'

	jmp $

[bits 32]
loader_protection:
	mov ax, SELECTOR_DATA
	mov ds, ax
	mov ss, ax
	mov es, ax
	mov esp, LOADER_STACK_TOP
	mov ax, SELECTOR_VIDEO
	mov gs, ax

	mov byte [gs:320], 'P'

	; 从磁盘的KERNEL_START_SECTOR扇区加载到内存KERNEL_BIN_BASE_ADDR
	mov eax, KERNEL_START_SECTOR
	mov ebx, KERNEL_BIN_BASE_ADDR
	mov ecx, 200
	call read_disk_m_32

	call setup_page

	sgdt [gdt_ptr]

	; GDTR寄存器低16位是gdt界限 其余才是起始地址
	; 也就是说ebx现在放入的是gdt起始地址
	mov ebx, [gdt_ptr + 2]
	add dword [ebx], 0xc0000000
	; 视频段是第三个段描述符，每个段描述符8个字节，所以24
	; 因为是LSB，所以地址越高，就是高字节
	; 段描述符中高4字节的最高位是段基址32～24，所以加4
	or dword [ebx + 24 + 4], 0xc0000000

	add esp, 0xc0000000

	; 将页起始地址装入cr3
	mov eax, PAGE_DIR_TABLE_POS
	mov cr3, eax

	; 开启cr0中的pg位，即开启分页模式
	mov eax, cr0
	or eax, 0x80000000
	mov cr0, eax

   ;在开启分页后,用gdt新的地址重新加载
   lgdt [gdt_ptr]             

   mov byte [gs:480], 'V'     ;视频段段基址已经被更新,用字符v表示virtual addr

   jmp SELECTOR_CODE:enter_kernel

enter_kernel:
	call kernel_init
	mov esp, KERNEL_STACK_POINTER

	jmp KERNEL_ENTRY_ADDR

setup_page:
	; 页目录表有1024个项，每个项占用4字节，所以需要4K字节
	mov ecx, 4096
	mov esi, PAGE_DIR_TABLE_POS
.loop_clear_page_dir_mem:
	mov byte [esi], 0
	inc esi
	loop .loop_clear_page_dir_mem

.create_pde:
	mov eax, PAGE_DIR_TABLE_POS
	; 此时eax指向第一个页表的地址
	add eax, 0x1000
	mov ebx, eax
	; 将eax中存放的页表的属性设置为：用户特权级，可写，存在
	or eax, PG_US_U | PG_RW_W | PG_P
	; 第0个和第768个页目录项同时指向第一个页表
	mov [PAGE_DIR_TABLE_POS + 0x0], eax
	; 3072 / 4 = 768
	mov [PAGE_DIR_TABLE_POS + 3072], eax
	; 最后一个页目录项指向页目录本身，这样可以通过虚拟地址访问到页表
	sub eax, 0x1000
	; 4092 / 4 = 1023
	mov [PAGE_DIR_TABLE_POS + 4092], eax

	; 1M内核低端内存 / 4k = 256
	; 初始化256个页表
	mov ecx, 256
	mov esi, 0
	mov edx, PG_US_U | PG_RW_W | PG_P
.create_pte:
	mov [ebx + esi * 4], edx
	; 每个页表地址与物理地址一一对应
	add edx, 4096
	inc esi
	loop .create_pte

	; 创建内核其他页表的PDE
	mov eax, PAGE_DIR_TABLE_POS
	add eax, 0x2000
	or eax, PG_US_U | PG_RW_W | PG_P
	mov ebx, PAGE_DIR_TABLE_POS
	mov ecx, 254
	mov esi, 769
	
.create_kernel_pde:
	mov [ebx + esi * 4], eax
	inc esi
	add eax, 4096
	loop .create_kernel_pde
	ret




kernel_init:
	mov eax, 0
	mov ebx, 0
	mov ecx, 0
	mov edx, 0

	; 获取程序头表目录大小
	mov dx, [KERNEL_BIN_BASE_ADDR + ELF_OF_EHENTSIZE]
	; 获取程序头表在elf文件中的偏移地址
	mov ebx, [KERNEL_BIN_BASE_ADDR + ELF_OF_PHOFF]
	; 获取程序头表实际在内存中的位置
	add ebx, KERNEL_BIN_BASE_ADDR
	mov cx, [KERNEL_BIN_BASE_ADDR + ELF_OF_PHNUM]

.load_kernel_segment:
	cmp byte [ebx + ELF_PHOF_TYPE], 0
	je .error_segment_type
	; 压入段大小
	push dword [ebx + ELF_PHOF_FILESZ]
	; 获取段在文件内的偏移地址
	mov eax, [ebx + ELF_PHOF_OFFSET]
	; 计算内存中的实际地址
	add eax, KERNEL_BIN_BASE_ADDR
	push eax
	; 压入段应在哪个地址
	push dword [ebx + ELF_PHOF_VADDR]
	call mem_cpy
	add esp, 12
.error_segment_type:
	add ebx, edx
	loop .load_kernel_segment
	ret


mem_cpy:
	; 将esp的值赋值给ebp
	; 由于不知道ebp的值是否有用，所以入栈保存
	push ebp
	mov ebp, esp
	; 外面还有ecx的循环，所以入栈保存
	push ecx
	
	; 由于上面入栈了8个字节，所以从ebp + 8为基址查找
	add ebp, 8
	; 由于栈从高到低发展，入栈遵循从右到左入栈
	mov edi, [ebp + 0]
	mov esi, [ebp + 4]
	mov ecx, [ebp + 8]
	; 逐字节拷贝
	rep movsb
	; 出栈ecx和ebp
	pop ecx
	pop ebp

	ret

;-------------------------------------------------------------------------------
			   ;功能:读取硬盘n个扇区
read_disk_m_32:	   
;-------------------------------------------------------------------------------
							 ; eax=LBA扇区号
							 ; ebx=将数据写入的内存地址
							 ; ecx=读入的扇区数
      mov esi,eax	   ; 备份eax
      mov di,cx		   ; 备份扇区数到di
;读写硬盘:
;第1步：设置要读取的扇区数
      mov dx,0x1f2
      mov al,cl
      out dx,al            ;读取的扇区数

      mov eax,esi	   ;恢复ax

;第2步：将LBA地址存入0x1f3 ~ 0x1f6

      ;LBA地址7~0位写入端口0x1f3
      mov dx,0x1f3                       
      out dx,al                          

      ;LBA地址15~8位写入端口0x1f4
      mov cl,8
      shr eax,cl
      mov dx,0x1f4
      out dx,al

      ;LBA地址23~16位写入端口0x1f5
      shr eax,cl
      mov dx,0x1f5
      out dx,al

      shr eax,cl
      and al,0x0f	   ;lba第24~27位
      or al,0xe0	   ; 设置7～4位为1110,表示lba模式
      mov dx,0x1f6
      out dx,al

;第3步：向0x1f7端口写入读命令，0x20 
      mov dx,0x1f7
      mov al,0x20                        
      out dx,al

;;;;;;; 至此,硬盘控制器便从指定的lba地址(eax)处,读出连续的cx个扇区,下面检查硬盘状态,不忙就能把这cx个扇区的数据读出来

;第4步：检测硬盘状态
  .not_ready:		   ;测试0x1f7端口(status寄存器)的的BSY位
      ;同一端口,写时表示写入命令字,读时表示读入硬盘状态
      nop
      in al,dx
      and al,0x88	   ;第4位为1表示硬盘控制器已准备好数据传输,第7位为1表示硬盘忙
      cmp al,0x08
      jnz .not_ready	   ;若未准备好,继续等。

;第5步：从0x1f0端口读数据
      mov ax, di	   ;以下从硬盘端口读数据用insw指令更快捷,不过尽可能多的演示命令使用,
			   ;在此先用这种方法,在后面内容会用到insw和outsw等

      mov dx, 256	   ;di为要读取的扇区数,一个扇区有512字节,每次读入一个字,共需di*512/2次,所以di*256
      mul dx
      mov cx, ax	   
      mov dx, 0x1f0
  .go_on_read:
      in ax,dx		
      mov [ebx], ax
      add ebx, 2
			  ; 由于在实模式下偏移地址为16位,所以用bx只会访问到0~FFFFh的偏移。
			  ; loader的栈指针为0x900,bx为指向的数据输出缓冲区,且为16位，
			  ; 超过0xffff后,bx部分会从0开始,所以当要读取的扇区数过大,待写入的地址超过bx的范围时，
			  ; 从硬盘上读出的数据会把0x0000~0xffff的覆盖，
			  ; 造成栈被破坏,所以ret返回时,返回地址被破坏了,已经不是之前正确的地址,
			  ; 故程序出会错,不知道会跑到哪里去。
			  ; 所以改为ebx代替bx指向缓冲区,这样生成的机器码前面会有0x66和0x67来反转。
			  ; 0X66用于反转默认的操作数大小! 0X67用于反转默认的寻址方式.
			  ; cpu处于16位模式时,会理所当然的认为操作数和寻址都是16位,处于32位模式时,
			  ; 也会认为要执行的指令是32位.
			  ; 当我们在其中任意模式下用了另外模式的寻址方式或操作数大小(姑且认为16位模式用16位字节操作数，
			  ; 32位模式下用32字节的操作数)时,编译器会在指令前帮我们加上0x66或0x67，
			  ; 临时改变当前cpu模式到另外的模式下.
			  ; 假设当前运行在16位模式,遇到0X66时,操作数大小变为32位.
			  ; 假设当前运行在32位模式,遇到0X66时,操作数大小变为16位.
			  ; 假设当前运行在16位模式,遇到0X67时,寻址方式变为32位寻址
			  ; 假设当前运行在32位模式,遇到0X67时,寻址方式变为16位寻址.

      loop .go_on_read
      ret

